package redislock

import (
	"context"
	"errors"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"log"
	"sync/atomic"
	"time"
)

// RedisLock redis锁
type RedisLock struct {
	client             redis.UniversalClient // redis客户端
	key                string                // 键
	token              string                // 身份标识
	ttl                int64                 // 存活时间（单位：秒）
	ttlListenCancel    context.CancelFunc    // ttl 监听取消函数
	ttlTimeoutCallback func()                // ttl 超时回调函数
	LockOption
}

// 获取存活时间（单位：秒）
func (r *RedisLock) TTL() int64 {
	return r.ttl
}

// DoRedisLock 获取锁并执行回调函数
func DoRedisLock(ctx context.Context, client redis.UniversalClient, key string, ttl int64, f func() error, opts ...Option) error {
	r := NewRedisLock(client, key, ttl, opts...)
	if err := r.Lock(ctx); err != nil {
		return err
	}
	defer func() {
		if unlockErr := r.Unlock(ctx); unlockErr != nil {
			// 如果解锁失败，可以记录日志，但不要覆盖原始 err
		}
	}()
	return f()
}

// NewRedisLock redis锁
func NewRedisLock(client redis.UniversalClient, key string, ttl int64, opts ...Option) *RedisLock {
	if ttl <= 0 {
		ttl = defaultTTLSeconds
	} else if ttl < minTTLSeconds {
		ttl = minTTLSeconds
	}

	lock := &RedisLock{
		client: client,
		key:    generateKey(key),
		token:  generateToken(),
		ttl:    ttl,
	}
	if len(opts) > 0 {
		for _, opt := range opts {
			if opt != nil {
				opt(lock)
			}
		}
	}
	return lock
}

// Lock 加锁
func (r *RedisLock) Lock(ctx context.Context) (err error) {
	var ok bool
	if ok, err = r.tryLock(ctx); err != nil {
		return err
	} else if !ok {
		if r.retryLock {
			// 轮询获取取锁
			if err = r.retry(ctx); err != nil {
				return
			}
		} else {
			return ErrAcquiredByOtherLock
		}
	}
	// 上锁成功，启动看门狗
	if !r.watchDogDisable {
		r.watchDog(ctx)
	} else {
		go r.listenTTL(ctx)
	}

	return
}

// Unlock 解锁
func (r *RedisLock) Unlock(ctx context.Context) error {
	// 解锁
	result, err := r.client.Eval(
		ctx,
		LuaCheckAndDeleteLock, // 脚本
		[]string{r.key},       // key 要用 string 切片
		r.token,               // args 是变长参数，直接写在后面
	).Result()
	if err != nil {
		return err
	}
	if ret, ok := result.(int64); !ok || ret != 1 {
		// 锁不属于自己，释放本地资源
		r.cleanup()
		return ErrOwnershipMismatchUnlock
	}
	// 解锁成功，释放本地资源
	r.cleanup()
	return nil
}

// 获取锁
func (r *RedisLock) tryLock(ctx context.Context) (bool, error) {
	return r.client.SetNX(ctx, r.key, r.token, time.Duration(r.ttl)*time.Second).Result()
}

// 重试加锁
func (r *RedisLock) retry(ctx context.Context) error {
	c, cancel := context.WithTimeout(ctx, time.Duration(r.retryTimeout)*time.Second)
	defer cancel()

	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()
	for {
		select {
		case <-c.Done():
			if errors.Is(c.Err(), context.DeadlineExceeded) {
				return ErrRetryLockAcquireTimeout
			}
			return errors.Join(ErrRetryLockAcquireFailed, c.Err())
		case <-ticker.C:
			if ok, err := r.tryLock(c); err != nil {
				return err
			} else if ok {
				return nil
			}
		}
	}
}

// 监听 ttl
func (r *RedisLock) listenTTL(ctx context.Context) {
	if r.ttl <= 0 {
		return
	}
	c, cancel := context.WithTimeout(ctx, time.Duration(r.ttl)*time.Second)
	r.ttlListenCancel = cancel
	select {
	case <-c.Done():
		if errors.Is(c.Err(), context.DeadlineExceeded) {
			if r.ttlTimeoutCallback != nil {
				r.ttlTimeoutCallback()
			} else {
				log.Printf("TTL 超时（未设置回调函数），key=%s，token=%s", r.key, r.token)
			}
		}
	}
}

// 启动看门狗
func (r *RedisLock) watchDog(ctx context.Context) {
	for !atomic.CompareAndSwapInt32(&r.watchDogActive, 0, 1) {
		return
	}
	c, cancel := context.WithCancel(ctx)
	r.watchDogCancel = cancel

	renewInterval := r.ttl / 2
	if renewInterval < 1 {
		renewInterval = 1
	}
	ticker := time.NewTicker(time.Duration(renewInterval) * time.Second)
	defer ticker.Stop()
	go func() {
		defer atomic.StoreInt32(&r.watchDogActive, 0)
		for {
			select {
			case <-c.Done():
				return
			case <-ticker.C:
				// 续约过期时间
				_ = r.delayExpire(c, r.ttl)
			}
		}
	}()
}

// 续约过期时间
func (r *RedisLock) delayExpire(ctx context.Context, expireSeconds int64) error {
	result, err := r.client.Eval(
		ctx,
		LuaCheckAndExpireLock,  // Lua 脚本
		[]string{r.key},        // KEYS[1]
		r.token, expireSeconds, // ARGV[1], ARGV[2]
	).Result()

	if err != nil {
		return err
	}

	if ret, ok := result.(int64); !ok || ret != 1 {
		return ErrOwnershipMismatchRenew
	}

	return nil
}

// 释放资源
func (r *RedisLock) cleanup() {
	if r.watchDogCancel != nil {
		r.watchDogCancel()
		r.watchDogCancel = nil
	}
	if r.ttlListenCancel != nil {
		r.ttlListenCancel()
		r.ttlListenCancel = nil
	}
}

// 生成键
func generateKey(key string) string {
	return redisLockKeyPrefix + key
}

// 生成身份标识
func generateToken() string {
	return uuid.NewString()
}
